#include "PlayerObject.h"
#include "EmptyObject.h"
#include "DirtObject.h"
#include "StoneObject.h"
#include "DiamondObject.h"
#include "WallObject.h"
#include "GateObject.h"
#include "Engine.h"
#include "TitleScreen.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <SDL_image.h>

enum {
  EVENT_TICK = 0
};

#define EVENT_INTERVAL 200 // Milisekundy
#define STONE_MOVE 5
#define GFX_GAMEOVER "gfx/gameover.png"


static Uint32
engine_tick(Uint32 interval, void *param)
{
  // Ta funkcja ma jedynie wysylac "tickniecie" do glownego silnika gry
  SDL_Event event;

  event.type = SDL_USEREVENT;
  event.user.type = SDL_USEREVENT;
  event.user.code = EVENT_TICK;

  SDL_PushEvent(&event);
  return interval;
}


// #pragma mark -


Engine::Engine()
  : fErrorCode(E_OK),
    fTimer(0),
    fTitleScreen(0),
    fMapInitialized(false)
{
  Uint32 flags;
  fErrorCode = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);
  if(fErrorCode != E_OK)
    return;

  fVideoInfo = SDL_GetVideoInfo();
  if (fVideoInfo->hw_available) {
    flags = SDL_RESIZABLE | SDL_HWPALETTE | SDL_HWSURFACE | SDL_DOUBLEBUF;
  }
  else {
    flags = SDL_RESIZABLE | SDL_SWSURFACE | SDL_DOUBLEBUF;
  }

  fScreen = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT,
      fVideoInfo->vfmt->BytesPerPixel >> 3, flags);
  if (!fScreen) {
    fErrorCode = E_ERROR;
    return;
  }

  SDL_ShowCursor(SDL_DISABLE);

  fErrorCode = TTF_Init();
  if (fErrorCode != E_OK)
    return;

  fFont = TTF_OpenFont("FreeSans.ttf", 20);
  if (!fFont) {
    fErrorCode = E_FONT_FAILED;
    return;
  }

  fGUI = IMG_Load("gfx/gui.png");
  if (!fGUI) {
    fErrorCode = E_ERROR;
    return;
  }

  // Preload graphics
  fGameOverGfx = IMG_Load(GFX_GAMEOVER);
  if (!fGameOverGfx)
    fErrorCode = E_ERROR;
}

Engine::~Engine()
{
  int i, size = fDrawQueue.size();
  for (i = 0;i < size;i++)
    fDrawQueue.pop();

  // Usuwanie elementow zwiazanych z silnikiem
  if (fTitleScreen)
    delete fTitleScreen;
}

int
Engine::LoadMap(const char *filename)
{
  fMapInitialized = false;
  ifstream file(filename);

  file.exceptions(ios::eofbit | ios::badbit | ios::failbit);

  fDiamonds = 0;
  fPlayer = 0;

  try
  {
    unsigned int x = 0, y = 0;
    for (x = 0;x < fMapWidth;x++) {
      for (y = 0;y < fMapHeight;y++)
      {
        if (fObjects[x][y])
          delete fObjects[x][y];
        fObjects[x][y] = 0;
      }
    }
    fObjects.clear();
    
    file >> fMapWidth;
    file >> fMapHeight;
    file >> fTimeLimit;
    
    fObjects.resize(fMapWidth, vector<Object *>(fMapHeight, 0));

    char buffer;
    x = 0;
    y = 0;

    file.read(&buffer, sizeof(char));
    while (file.read(&buffer, sizeof(char))) {

      switch (buffer) {
        case '.':
          AddObject(x, y, new DirtObject(this, x, y));
          break;
        case ' ':
          AddObject(x, y, new EmptyObject(this, x, y));
          break;
        case 'o':
          AddObject(x, y, new StoneObject(this, x, y));
          break;
        case '*':
          AddObject(x, y, new DiamondObject(this, x, y));
          fDiamonds++;
          break;
        case '\n':
          x = 0;
          y++;
          continue;
        case '#':
          AddObject(x, y, new WallObject(this, x, y));
          break;
        case 'S':
          fPlayer = new PlayerObject(this, x, y);
          AddObject(x, y, fPlayer);
          break;
        case 'E':
          // TODO
          AddObject(x, y, new GateObject(this, x, y));
          break;
        default:
          cerr << "Invalid character in the given map file!" << endl;
          throw int(E_MAP_ERROR);
      }
      x++;
    }

  }
  catch (ios::failure eol) {
  }

  fMapInitialized = true;
  fErrorCode = E_OK;
  return E_OK;
}

void
Engine::AddObject(unsigned int x, unsigned int y, Object *object)
{
  if (fObjects[x][y]) 
    delete fObjects[x][y];
  fObjects[x][y] = object;
  object->SetPosition(x, y);
  _AddToDrawQueue(object);
}

void
Engine::RemoveObject(unsigned int x, unsigned int y)
{
  if (fObjects[x][y])
    delete fObjects[x][y];
  fObjects[x][y] = new EmptyObject(this, x, y);
}

void
Engine::Move(unsigned int srcX, unsigned int srcY, unsigned int destX,
        unsigned int destY)
{
  if (fObjects[destX][destY])
    delete fObjects[destX][destY];

  Object *object = fObjects[srcX][srcY];
  fObjects[destX][destY] = object;
  object->SetPosition(destX, destY);
  _AddToDrawQueue(object);

  object = new EmptyObject(this, srcX, srcY);
  fObjects[srcX][srcY] = object;
  _AddToDrawQueue(object);
}

int 
Engine::Run()
{
  if (fErrorCode < E_OK)
    return fErrorCode;

  if (!fTitleScreen)
    fTitleScreen = new TitleScreen(this);
  fKeysPressed = 0;
  fErrorCode = fTitleScreen->Run();
  if (fErrorCode < E_OK)
    return fErrorCode;
  else if (fErrorCode == E_DONE)
    return E_DONE;

  fTicks = 0;
  fLevel = 0;

  fMapWidth = 0;
  fMapHeight = 0;

  try
  {
    while (fErrorCode != E_DONE) {
      fAlive = true;
      stringstream mapName;
      mapName << "map" << (int)fLevel << ".mbm";
      LoadMap(mapName.str().c_str());

      if (!fPlayer)
        throw int(E_ERROR);

      if (fMapHeight <= GRID_HEIGHT) {
        fScreenPosY = - (GRID_HEIGHT - fMapHeight) / 2;
      }
      else {
        _PositionCameraY();
      }

      if (fMapWidth <= GRID_WIDTH) {
        fScreenPosX = - ((GRID_WIDTH - fMapWidth) / 2);
      }
      else {
        _PositionCameraX();
      }

      _DrawGUI();

      // Dodanie "ticku"
      fTimer = SDL_AddTimer(EVENT_INTERVAL, engine_tick, 0);
      if (!fTimer)
        throw int(E_ERROR);

      SDL_Event event;

      // Glowna petla programu
      while (fErrorCode == E_OK)
      {
        if(SDL_WaitEvent(NULL)) {
          while(SDL_PollEvent(&event)) {
            switch(event.type) {
              case SDL_USEREVENT:
                _HandleTick();
                break;
              case SDL_KEYDOWN:
                fKeysPressed++;
                fKey = event.key.keysym.sym;
                if (fKey == SDLK_ESCAPE)
                  fErrorCode = E_DONE;
                break;
              case SDL_KEYUP:
                fKeysPressed--;
                fMoveStarted = 0;
                break;
              case SDL_QUIT:
                fErrorCode = E_DONE;
                break;
            }
          }
        }
      }

      fPlayer = 0;
      if (fErrorCode == E_NEXT_LEVEL) {
        fLevel++;
        if (fLevel > 4) {
          fErrorCode = fTitleScreen->Run();
          if (fErrorCode < E_OK)
            return fErrorCode;
          else if (fErrorCode == E_DONE)
            return E_DONE;
        }
      }

      SDL_RemoveTimer(fTimer);
      fTimer = 0;
    }
  }
  catch(int error) {
    cerr << "Error (code: " << error << ")" << endl;
    fErrorCode = error;
    return error;
  }

  return E_DONE;
}


// #pragma mark -


void 
Engine::_AddToDrawQueue(Object *object) 
{
  if(object) 
    fDrawQueue.push(object); 
}

void
Engine::_HandleTick()
{
  if (!fAlive) {
    fTicks++;
    if (fTicks == 15) {
      SDL_BlitSurface(fGameOverGfx, 0, fScreen, 0);
      SDL_Flip(fScreen);
    }

    if (fTicks >= 25) {
      fErrorCode = E_DONE;
    }
    return;
  }

  fTicks++;
  if (fTicks >= 5) {
    fTicks = 0;
    fTimeLimit--;

    fNeedGUIRedraw = true;
    if (fTimeLimit == 0) {
      fAlive = false;
      fTicks = 14;
      //fErrorCode = E_DONE;
      return;
    }
  }

  if (fRecheck) {
    _MoveObjects();
  }

  bool moved = false;
  if (fKeysPressed > 0) {
    unsigned int x = fPlayer->x, y = fPlayer->y;
    switch(fKey) {
      case SDLK_UP:
        if (y > 0)
          y--;
        break;
      case SDLK_DOWN:
        if (y < fMapHeight)
          y++;
        break;
      case SDLK_RIGHT:
        if (x < fMapWidth)
          x++;
        break;
      case SDLK_LEFT:
        if (x > 0)
          x--;
        break;
      default:
        goto continue_tick;
        break;
    }

    if (fObjects[x][y]) {
      switch(fObjects[x][y]->Invoke()) {
        case I_MOVE_DIRT:
        case I_MOVE:
          Move(fPlayer->x, fPlayer->y, x, y);
          moved = true;
          break;
        case I_MOVE_OBJECT:
          fMoveStarted++;
          break;
        case I_DIAMOND:
          fDiamonds--;
          fNeedGUIRedraw = true;
          fMoveStarted = 0;
          Move(fPlayer->x, fPlayer->y, x, y);
          moved = true;
          break;
        case I_GATE:
          if (fDiamonds == 0) {
            Move(fPlayer->x, fPlayer->y, x, y);
            fErrorCode = E_NEXT_LEVEL;
            return;
          }
        default:
          fMoveStarted = 0;
          break;
      }
    }
    fRecheck = true;

    if (fMoveStarted >= STONE_MOVE) {
      if ((y - fPlayer->y) == 0) { 
        unsigned int destX = x + (x - fPlayer->x);
        if (fObjects[destX][y]->Invoke() == I_MOVE) {
          Move(x, y, destX, y);
          Move(fPlayer->x, fPlayer->y, x, y);
          moved = true;
        }
        fMoveStarted--;
      }
    }
  }

continue_tick:
  if (moved) {
    long int sX, sY;
    int i, j, numX, numY;
    // Ustawienie kamery w razie potrzeby
    if (fMapWidth > GRID_WIDTH) {
      long int x = fPlayer->x;
      if (x <= (fScreenPosX + 2) || x >= (fScreenPosX + GRID_WIDTH - 3)) {
        _PositionCameraX();
      }
      sX = fScreenPosX;
      numX = sX + GRID_WIDTH;
    }
    else {
      sX = 0;
      numX = fMapWidth;
    }

    if (fMapHeight > GRID_HEIGHT) {
      long int y = fPlayer->y;
      if (y <= (fScreenPosY + 2) || y >= (fScreenPosY + GRID_HEIGHT - 2)) {
        _PositionCameraY();
      }
      sY = fScreenPosY;
      numY = sY + GRID_HEIGHT;
    }
    else {
      sY = 0;
      numY = fMapHeight;
    }
    
    for (i = sX; i < numX;i++) {
      for (j = sY; j < numY;j++) {
        _AddToDrawQueue(fObjects[i][j]);
      }
    }
  }

  if (!fDrawQueue.empty())
    _DrawGamescreen();
  
  if (fNeedGUIRedraw) {
    _DrawGUI();
    fNeedGUIRedraw = false;
  }
}

void
Engine::_DrawGamescreen()
{
  bool needsFlip = false;
  int posX, posY, i, size = fDrawQueue.size();
  Object *iter;
  SDL_Surface *gfx;
  for (i = 0;i < size && iter;i++)
  {
    iter = fDrawQueue.front();
    fDrawQueue.pop();
    
    gfx = iter->GetGraphic();
    if (!gfx)
      throw int(E_ERROR);

    posX = iter->x - fScreenPosX;
    posY = iter->y - fScreenPosY;

    if (posX >= 0 && posX < GRID_WIDTH && posY >= 0 && posY < GRID_HEIGHT) {
      SDL_Rect rect = { posX * GFX_SIZE, posY * GFX_SIZE, 0, 0};
      SDL_BlitSurface(gfx, 0, fScreen, &rect);
      needsFlip = true;
    }
  }

  if (needsFlip)
    SDL_Flip(fScreen);
}

void
Engine::_DrawGUI()
{
  SDL_Rect rect = { 0, SCREEN_HEIGHT - GUI };
  SDL_BlitSurface(fGUI, 0, fScreen, &rect);

  SDL_Surface *text;
  SDL_Color white = {255, 255, 255};

  {
  stringstream label;
  label << "Diamonds left: " << (int)fDiamonds;

  text = TTF_RenderUTF8_Blended(fFont, label.str().c_str(), white);
  rect.x = 20;
  rect.y += 10;
  SDL_BlitSurface(text, 0, fScreen, &rect);
  delete text;
  }

  { 
    stringstream label;
    label << "Level: " << (int)(fLevel + 1);

    text = TTF_RenderUTF8_Blended(fFont, label.str().c_str(), white);
    rect.y += 30;
    SDL_BlitSurface(text, 0, fScreen, &rect);
    delete text;
  }

  stringstream label;
  label << "Time left: " << (int)fTimeLimit;
  text = TTF_RenderUTF8_Blended(fFont, label.str().c_str(), white);
  rect.x = SCREEN_WIDTH - 300;
  rect.y -= 30;
  SDL_BlitSurface(text, 0, fScreen, &rect);


  SDL_Flip(fScreen);

  delete text;
}

void
Engine::_MoveObjects()
{
  unsigned int x, y;
  list<Object *>::iterator end = fMovableObjects.end(), 
    iter = fMovableObjects.begin();

  while (iter != end) {
    x = (*iter)->x;
    y = (*iter)->y;
    
    // Przyklad jak moze byc uzyty dynamic_cast zeby sie dowiedziec
    // typu danego obiektu. Zamiast tego mozna bylo uzyc funkcji Invoke()

    if (y < fMapHeight) {
      if (dynamic_cast<EmptyObject *>(fObjects[x][y + 1])) {
        Move(x, y, x, y + 1);
        (*iter)->Move(true);
      }
      else if (dynamic_cast<PlayerObject *>(fObjects[x][y + 1])) {
        if ((*iter)->DidMove()) {
          //fErrorCode = E_DONE;
          fAlive = false;
          fPlayer->ToggleState();
          _AddToDrawQueue(fPlayer);
          return;
        }
      }
      else if (x > 0 || x < fMapWidth) {
        if (dynamic_cast<StoneObject *>(fObjects[x][y + 1]) ||
            dynamic_cast<DiamondObject *>(fObjects[x][y + 1])) {
          if (dynamic_cast<EmptyObject *>(fObjects[x - 1][y]) &&
            dynamic_cast<EmptyObject *>(fObjects[x - 1][y + 1])) {
            
            Move(x, y, x - 1, y);
            (*iter)->Move(true);
          }
          else if (dynamic_cast<EmptyObject *>(fObjects[x + 1][y]) &&
            dynamic_cast<EmptyObject *>(fObjects[x + 1][y + 1])) {
            
            Move(x, y, x + 1, y);
            (*iter)->Move(true);
          }
          else
            (*iter)->Move(false);
        }
        else
          (*iter)->Move(false);
      }
      else
        (*iter)->Move(false);
    }

    iter++;
  }
}

inline void
Engine::_PositionCameraX()
{
  fScreenPosX = fPlayer->x - (GRID_WIDTH / 2);
  if (fScreenPosX < 0) {
    fScreenPosX = 0;
  }
  else if ((fScreenPosX + GRID_WIDTH) > (long int)(fMapWidth)) {
    fScreenPosX = fMapWidth - GRID_WIDTH;
  }
}

inline void
Engine::_PositionCameraY()
{
  fScreenPosY = fPlayer->y - (GRID_HEIGHT/ 2);
  if (fScreenPosY < 0) {
    fScreenPosY = 0;
  }
  else if ((fScreenPosY + GRID_HEIGHT) > (long int)(fMapHeight)) {
    fScreenPosY = fMapHeight - GRID_HEIGHT;
  }
}
